package Game;

import java.util.ArrayList;

public abstract class Unit extends Card implements IFieldObject, ITargetable, IEffectTarget, IAffectable {
	// CONSTANTS
	
	// Basic stats constants
	public static final int STATS_STR = 0;
	public static final int STATS_INT = 1;
	public static final int STATS_CON = 2;
	public static final int STATS_SPD = 3;
	public static final int STATS_DEX = 4;
	
	// Stats calculation constants
	/**
	 * Number of Attack increased for each point of STR increased
	 */
	public static final int STR_TO_ATTACK = 1;
	/**
	 * Number of Block increased for each point of STR increased
	 */
	public static final int STR_TO_BLOCK = 1;
	/**
	 * Number of Magic increased for each point of INT increased
	 */
	public static final int INT_TO_MAGIC = 1;
	/**
	 * Number of Magic increased for each point of INT increased
	 */
	public static final int INT_TO_MP = 2;
	/**
	 * Number of Defense increased for each point of CON increased
	 */
	public static final int CON_TO_DEFENSE = 1;
	/**
	 * Number of HP increased for each point of CON increased
	 */
	public static final int CON_TO_HP = 3;
	/**
	 * Number of AP increased for each point of SPD increased
	 */
	public static final int SPD_TO_AP = 2;
	/**
	 * Number of Evasion increased for each point of SPD increased
	 */
	public static final int SPD_TO_EVASION = 1;
	/**
	 * Number of Accuracy increased for each point of DEX increased
	 */
	public static final int DEX_TO_ACCURACY = 2;
	/**
	 * Number of Critical Rate increased for each point of DEX increased
	 */
	public static final int DEX_TO_CRITICAL = 4;
	
	// AP constants
	public static final int DEFAULT_AP_MOVE = 3;
	
	// ATTRIBUTES
	
	int level = 1;
	Cell position;
	
	private int STR;
	private int INT;
	private int CON;
	private int SPD;
	private int DEX;
	
	private int Attack;
	private int Block;
	private int Defense;
	private int Magic;
	private int MaxMP;
	private int MP;
	private int MaxHP;
	private int HP;
	private int MaxAP;
	private int AP;
	private int Evasion;
	private int Accuracy;
	private int CriticalRate;
	
	int[] elementalResistance = new int[8];
	
	int AttackBonus = 0;
	int BlockBonus = 0;
	int DefenseBonus = 0;
	int MagicBonus = 0;
	int MaxMPBonus = 0;
	int MaxHPBonus = 0;
	int MaxAPBonus = 0;
	int EvasionBonus = 0;
	int AccuracyBonus = 0;
	
	int range;
	int attackElement = Element.ELEMENT_NORMAL;
	
	int moveAPCost = DEFAULT_AP_MOVE;
	int attackAPCost;
	
	ArrayList<AbstractSkill> skills = new ArrayList<>();
	ArrayList<Ability> abilites = new ArrayList<>();
	
	public Unit(String _ID) {
		super(_ID);
		initBaseStats();
	}
	
	public int getSTR() { return STR; }
	public void setSTR(int newSTR) {
		incSTR(newSTR - STR);
	}
	public void incSTR(int diff) {
		STR += diff;
		incAttack(diff*STR_TO_ATTACK);
		incBlock(diff*STR_TO_BLOCK);
	}
	
	public int getINT() { return INT; }
	public void setINT(int newINT) {
		incINT(newINT - INT);
	}
	public void incINT(int diff) {
		INT += diff;
		incMagic(diff*INT_TO_MAGIC);
		incMaxMP(diff*INT_TO_MP);
	}
	
	public int getCON() { return CON; }
	public void setCON(int newCON) {
		incCON(newCON - CON);
	}
	public void incCON(int diff) {
		CON += diff;
		incDefense(diff*CON_TO_DEFENSE);
		incMaxHP(diff*CON_TO_HP);
	}
	
	public int getSPD() { return SPD; }
	public void setSPD(int newSPD) {
		incSPD(newSPD - SPD);
	}
	public void incSPD(int diff) {
		SPD += diff;
		incMaxAP(diff*SPD_TO_AP);
		incEvasion(diff*SPD_TO_EVASION);
	}
	
	public int getDEX() { return DEX; }
	public void setDEX(int newDEX) {
		incDEX(newDEX - DEX);
	}
	public void incDEX(int diff) {
		DEX += diff;
		incAccuracy(diff*DEX_TO_ACCURACY);
		incCriticalRate(diff*DEX_TO_CRITICAL);
	}
	
	public int getAttack() {
		return Attack + Math.round(Attack*(float)AttackBonus/100);
	}
	public void setAttack(int newAttack) {
		incAttack(newAttack - Attack);
	}
	public void incAttack(int diff) {
		Attack += diff;
		
		if (Attack <= 0)
			Attack = 1;
	}
	
	public int getBlock() {
		return Block + Math.round(Block*(float)BlockBonus/100);
	}
	public void setBlock(int newBlock) {
		incBlock(newBlock - Block);
	}
	public void incBlock(int diff) {
		Block += diff;
		
		if (Block <= 0)
			Block = 1;
	}
	
	public int getMagic() {
		return Magic + Math.round(Magic*(float)MagicBonus/100);
	}
	public void setMagic(int newMagic) {
		incMagic(newMagic - Magic);
	}
	public void incMagic(int diff) {
		Magic += diff;
		
		if (Magic <= 0)
			Magic = 1;
	}
	
	public int getMaxMP() {
		return MaxMP + Math.round(MaxMP*(float)MaxMPBonus/100);
	}
	public void setMaxMP(int newMaxMP) {
		incMaxMP(newMaxMP - MaxMP);
	}
	public void incMaxMP(int diff) { 
		MaxMP += diff;
		incMP(diff);
	}
	
	public int getMP() { return MP; }
	public void setMP(int newMP) {
		incMP(newMP - MP);
	}
	public void incMP(int diff) {
		MP += diff;
		
		if (MP > MaxMP)
			MP = MaxMP;
		else if (MP < 0)
			MP = 0;
	}
	
	public int getDefense() {
		return Defense + Math.round(Defense*(float)DefenseBonus/100);
	}
	public void setDefense(int newDefense) {
		incDefense(newDefense - Defense);
	}
	public void incDefense(int diff) {
		Defense += diff;
		
		if (Defense <= 0)
			Defense = 1;
	}
	
	public int getMaxHP() {
		return MaxHP + Math.round(MaxHP*(float)MaxHPBonus/100);
	}
	public void setMaxHP(int newMaxHP) {
		incMaxHP(newMaxHP - MaxHP);
		
	}
	public void incMaxHP(int diff) {
		MaxHP += diff;
		incHP(diff);
	}
	
	public int getHP() { return HP; }
	public void setHP(int newHP) {
		incHP(newHP - HP);
	}
	public void incHP(int diff) {
		HP += diff;
		
		if (HP > MaxHP)
			HP = MaxHP;
		else if (HP < 0)
			HP = 0;
	}
	
	public int getMaxAP() {
		return MaxAP + Math.round(MaxAP*(float)MaxAPBonus/100);
	}
	public void setMaxAP(int newMaxAP) {
		incMaxAP(newMaxAP - MaxAP);
	}
	public void incMaxAP(int diff) {
		MaxAP += diff;
		incAP(diff);
	}
	
	public int getAP() { return AP; }
	public void setAP(int newAP) {
		incAP(newAP - AP);
	}
	public void incAP(int diff) {
		AP += diff;
		
		if (AP > MaxAP)
			AP = 0;
		else if (AP < 0)
			AP = 0;
	}
	
	public int getEvasion() {
		return Evasion + Math.round(Evasion*(float)EvasionBonus/100);
	}
	public void setEvasion(int newEvasion) {
		incEvasion(newEvasion - Evasion);
	}
	public void incEvasion(int diff) {
		Evasion += diff;
		
		if (Evasion < 0)
			Evasion = 0;
	}
	
	public int getAccuracy() {
		return Accuracy + Math.round(Accuracy*(float)AccuracyBonus/100);
	}
	public void setAccuracy(int newAccuracy) {
		incAccuracy(newAccuracy - Accuracy);
	}
	public void incAccuracy(int diff) {
		Accuracy += diff;
		
		if (Accuracy < 0)
			Accuracy = 0;
	}
	
	public int getCriticalRate() { return CriticalRate; }
	public void setCriticalRate(int newCriticalRate) {
		incCriticalRate(newCriticalRate - CriticalRate);
	}
	public void incCriticalRate(int diff) {
		CriticalRate += diff;
		
		if (CriticalRate < 0)
			CriticalRate = 0;
	}
	
	/**
	 * Levels up Unit and increases its stats
	 * @param stats Various stats to be increased. Each element in it corresponds to 
	 * one of the basic stats (STR, INT, CON, SPD, DEX) and will increase the corresponding stats by 1.
	 */
	public void levelUp(int... stats) {
		for (int stat: stats) {
			switch (stat) {
			case STATS_STR:
				incSTR(1);
				break;
			case STATS_INT:
				incINT(1);
				break;
			case STATS_CON:
				incCON(1);
				break;
			case STATS_SPD:
				incSPD(1);
				break;
			case STATS_DEX:
				incDEX(1);
				break;
			}
		}
	}
	
	private void initBaseStats() {
		switch (ID) {
		case Character.CHARACTER_HUMAN:
			setSTR(7);
			setINT(7);
			setCON(6);
			setSPD(5);
			setDEX(5);
			break;
		case Character.CHARACTER_ELF:
			setSTR(5);
			setINT(7);
			setCON(3);
			setSPD(8);
			setDEX(7);
			break;
		case Character.CHARACTER_DWARF:
			setSTR(8);
			setINT(4);
			setCON(8);
			setSPD(4);
			setDEX(6);
			break;
		case Character.CHARACTER_ORC:
			setSTR(9);
			setINT(5);
			setCON(8);
			setSPD(3);
			setDEX(5);
			break;
		case Character.CHARACTER_GOBLIN:
			setSTR(5);
			setINT(6);
			setCON(4);
			setSPD(7);
			setDEX(8);
			break;
		}
	}
	
	/**
	 * Places this Unit into destination. Destination Cell is assumed to be occupiable and unoccupied. 
	 * @param destination Cell into which this Unit is being placed
	 */
	public void place(Cell destination) {
		position = destination;
	}
	
	/**
	 * Moves this Unit to destination. Destination Cell is assumed to be adjacent to this Unit's
	 * current position. Other move requirements (e.g. AP cost) are also assumed to be met. 
	 * @param destination Destination Cell, adjacent to this Unit's current position.
	 */
	public void moveOnce(Cell destination) {
		if (position.hasMoveFromEffect())
			position.cellEffect.onObtain(this);
		
		incAP(-moveAPCost);
		position = destination;
		if (position.hasMoveIntoEffect())
			position.cellEffect.onObtain(this);
	}
	
	/**
	 * Moves this Unit along some path. Each Cell in the is adjacent to the Cell before it.
	 * Other move requirements (e.g. AP cost) are assumed to be met.
	 * @param path Ordered list of Cell which this Unit will move into, step by step.
	 */
	public void move(ArrayList<Cell> path) {
		while (!path.isEmpty()) {
			moveOnce(path.remove(0));
		}
	}
	
	public void attack(Unit target) {
		Damage damage = new Damage(getAttack(), this, target);
		
		for (Effect effect: effects) {
			if (effect.isDamageEffect() && effect.isConditionMet(damage))
				effect.onActivate(damage);
		}
		
		incAP(-attackAPCost);
		damage.incur();
	}
	
	@Override
	public String toString() {
		String s = "";
		
		s += "ID   : " + ID;
		s += "\nName : " + name;
		s += "\nLevel: " + level;
		s += "\nHP   : " + getMaxHP() + "/" + getHP();
		s += "\nMP   : " + getMaxMP() + "/" + getMP();
		s += "\nAP   : " + getMaxAP() + "/" + getAP();
		
		s += "\n\nSTR: " + getSTR();
		s += "\nINT: " + getINT();
		s += "\nCON: " + getCON();
		s += "\nSPD: " + getSPD();
		s += "\nDEX: " + getDEX();
		
		s += "\n\nAttack       : " + getAttack();
		s += "\nBlock        : " + getBlock();
		s += "\nDefense      : " + getDefense();
		s += "\nMagic        : " + getMagic();
		s += "\nEvasion      : " + getEvasion();
		s += "\nAccuracy     : " + getAccuracy();
		s += "\nCritical Rate: " + getCriticalRate() + "%";
		
//		s += "\n\nFire Resistance   : " + FireResistance + "%";
//		s += "\nWater Resistance  : " + WindResistance + "%";
//		s += "\nEarth Resistance  : " + EarthResistance + "%";
//		s += "\nThunder Resistance: " + ThunderResistance + "%";
//		s += "\nWater Resistance  : " + WaterResistance + "%";
		
		s += "\n\nEffects:\n";
		int i = 1;
		for (Effect effect: effects) {
			s += i++ + ". " + effect + "\n";
		}
		
		return s;
	}
	
	public static void main(String[] args) {
		
	}
}
